iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
自我挑戰組

AI 不只會回答:30 天打造一套真正能上線的智慧助理系列 第 6

[Day 6] 串接 Vector Search 與 LLM 讓 AI 助理真正看著資料回答問題

  • 分享至 

  • xImage
  •  

[Day 6] 串接 Vector Search 與 LLM 讓 AI 助理真正看著資料回答問題

今天為什麼要做這件事

前幾天我們已經完成 RAG 的前置工作:

  • Day 3:將文件切分成多個 Chunk
  • Day 4:使用 Embedding 將文字轉換成向量
  • Day 5:建立 Vector Database,並進行相似度搜尋

目前系統已經能夠根據使用者的問題,從知識庫中找到相關內容

但是,還有一個重要問題:

搜尋到的資料,如何交給 LLM,讓它理解內容並產生自然的回答

如果只使用 Vector Search,我們得到的通常是幾段文字,而不是完整的對話答案

因此,今天要把 Vector Search 與 LLM 串接起來
完成第一個可以根據知識庫回答問題的 RAG Assistant

今天要解決什麼問題

假設知識庫中有以下內容:

本產品支援 Windows 10、Windows 11 以及 Ubuntu 22.04 與 Ubuntu 24.04

使用者可以透過官方網站下載安裝程式

安裝完成後,需要重新啟動系統

當使用者提問:

這個產品支援哪些作業系統

系統應該執行以下流程:

  1. 接收使用者問題
  2. 將問題轉換成 Embedding
  3. 從 Vector Database 搜尋相關內容
  4. 將搜尋結果組合成 Context
  5. 將問題與 Context 傳給 LLM
  6. 產生根據文件內容的回答

最終回答可能是:

本產品支援 Windows 10、Windows 11、Ubuntu 22.04 以及 Ubuntu 24.04

這就是 RAG 的核心概念:

先從知識庫取得相關資訊,再讓 LLM 根據這些資訊產生回答

RAG 的完整架構

前幾天我們將 RAG 拆成兩個階段:

文件建立階段

  • 文件
  • Chunking
  • Embedding
  • Vector Database

這個階段主要負責建立可搜尋的知識庫

使用者查詢階段

  • 使用者問題
  • Query Embedding
  • Vector Search
  • 取得相關文件
  • 組合 Prompt
  • LLM 產生回答

其中,今天的重點是:

Relevant Chunks → Prompt → LLM → Final Answer


Retrieval 與 Generation 的分工

RAG 主要由兩個部分組成

  • Retrieval: 從知識庫中搜尋相關資訊
  • Generation: 根據相關資訊產生自然語言的回答

Retrieval

負責:

知識庫中有哪些內容可能與使用者問題有關

例如:

使用者問題:
這個產品支援哪些作業系統

搜尋結果:
本產品支援 Windows 10、Windows 11 以及 Ubuntu 22.04 與 Ubuntu 24.04

Generation

負責:

如何根據搜尋結果,產生使用者看得懂的回答

例如:

本產品支援 Windows 10、Windows 11、Ubuntu 22.04 以及 Ubuntu 24.04

需要注意的是:

Vector Search 不會直接產生完整答案,LLM 也不應該在沒有參考資料的情況下隨意補充資訊

今天使用的技術

本次實作延續 Day 5 的技術:

使用 Gemini API 作為示範

實際使用哪一種 LLM,並不會改變 RAG 的核心流程

安裝相關套件

ChromaDB、Embedding 模型與 Gemini SDK:

pip install chromadb sentence-transformers langchain-text-splitters google-genai python-dotenv

設定 API Key

在專案根目錄建立 .env
API Key 應該透過環境變數讀取

GEMINI_API_KEY=your_api_key

Step 3:建立 LLM Client

import os

from dotenv import load_dotenv
from google import genai

load_dotenv()

api_key = os.getenv("GEMINI_API_KEY")

if not api_key:
    raise ValueError("找不到 GEMINI_API_KEY")

client = genai.Client(api_key=api_key)

這裡先建立 Gemini Client,後續可以透過它呼叫模型

實際使用的模型名稱,應依照目前 API 可用模型與帳號權限進行設定

Step 4:建立 Vector Database

from pathlib import Path

import chromadb
from langchain_text_splitters import (
    RecursiveCharacterTextSplitter,
)
from sentence_transformers import SentenceTransformer


DATA_PATH = Path("data/sample.txt")
CHROMA_PATH = "./data/chroma"


def create_collection():
    client = chromadb.PersistentClient(path=CHROMA_PATH)

    collection = client.get_or_create_collection(
        name="knowledge_base"
    )

    return collection


def load_chunks():
    text = DATA_PATH.read_text(encoding="utf-8")

    splitter = RecursiveCharacterTextSplitter(
        chunk_size=100,
        chunk_overlap=20
    )

    chunks = splitter.split_text(text)

    return chunks


def build_vector_database():
    collection = create_collection()
    chunks = load_chunks()

    model = SentenceTransformer(
        "all-MiniLM-L6-v2"
    )

    embeddings = model.encode(chunks)

    collection.upsert(
        ids=[
            f"chunk-{i}"
            for i in range(len(chunks))
        ],
        documents=chunks,
        embeddings=embeddings.tolist(),
        metadatas=[
            {"source": "sample.txt"}
            for _ in chunks
        ]
    )

    return collection

Step 5:建立 Vector Search

def search_documents(query, n_results=2):
    collection = create_collection()

    model = SentenceTransformer(
        "all-MiniLM-L6-v2"
    )

    query_embedding = model.encode([query])

    results = collection.query(
        query_embeddings=query_embedding.tolist(),
        n_results=n_results
    )

    documents = results.get("documents", [[]])[0]

    return documents

測試搜尋:

if __name__ == "__main__":
    build_vector_database()

    query = "這個產品支援哪些作業系統"

    documents = search_documents(query)

    for i, document in enumerate(documents):
        print(f"\n結果 {i + 1}")
        print(document)

可能得到類似結果:

結果 1
本產品支援 Windows 10、Windows 11 以及 Ubuntu 22.04 與 Ubuntu 24.04

搜尋結果會依照向量相似度排序

不過,搜尋結果只是文字片段,還需要交給 LLM 進行理解與組織

Step 6:建立 Context

LLM 不一定需要知道 Vector Database 的內部結構

我們只需要將搜尋到的文件內容整理成一段 Context

def build_context(documents):
    context = "\n\n".join(documents)

    return context

例如:

documents = [
    "本產品支援 Windows 10、Windows 11",
    "本產品支援 Ubuntu 22.04 與 Ubuntu 24.04"
]

context = build_context(documents)

print(context)

輸出:

本產品支援 Windows 10、Windows 11

本產品支援 Ubuntu 22.04 與 Ubuntu 24.04

Context 就是接下來提供給 LLM 參考的資料。

Step 7:設計 RAG Prompt

建立一個基本的 Prompt Template:

def build_prompt(query, context):
    prompt = f"""
你是一個專業的 AI 智慧助理

請根據下方提供的參考資料回答使用者問題

回答規則:
1. 只能根據參考資料回答
2. 如果參考資料沒有相關資訊,請明確說明目前資料不足
3. 不要自行捏造文件中沒有提到的資訊
4. 使用繁體中文回答
5. 回答要清楚、簡潔

參考資料:
{context}

使用者問題:
{query}

請提供回答:
"""

    return prompt

這裡的 Prompt 主要包含三個部分:

  1. 角色設定:告訴模型要扮演什麼角色
  2. 參考資料:提供從 Vector Search 取得的內容
  3. 使用者問題:要求模型根據資料回答

Step 8:呼叫 LLM

from src.llm.client import client
from src.rag.vector_db import search_documents
from src.rag.vector_db import build_vector_database


MODEL_NAME = "gemini-2.5-flash"


def build_context(documents):
    return "\n\n".join(documents)


def build_prompt(query, context):
    return f"""
你是一個專業的 AI 智慧助理。

請根據下方提供的參考資料回答使用者問題。

回答規則:
1. 只能根據參考資料回答。
2. 如果資料不足,請明確說明。
3. 不要捏造參考資料沒有提到的內容。
4. 使用繁體中文回答。

參考資料:
{context}

使用者問題:
{query}

請提供回答:
"""


def ask_rag(query):
    documents = search_documents(
        query=query,
        n_results=2
    )

    context = build_context(documents)

    prompt = build_prompt(
        query=query,
        context=context
    )

    response = client.models.generate_content(
        model=MODEL_NAME,
        contents=prompt
    )

    return {
        "answer": response.text,
        "documents": documents
    }


if __name__ == "__main__":
    build_vector_database()

    query = "這個產品支援哪些作業系統"

    result = ask_rag(query)

    print("回答:")
    print(result["answer"])

    print("\n參考文件:")

    for document in result["documents"]:
        print(document)

實驗測試

在實際結果仍可能受到以下因素影響:

  • 搜尋到的文件是否相關
  • Prompt 設計
  • 模型本身的推理與生成行為
  • Context 是否包含足夠資訊

因此,Prompt 指示不能取代完整的驗證機制


今天遇到的問題

問題一:LLM 可能忽略參考資料

即使 Prompt 要求模型只能根據文件回答,LLM 仍可能使用自身訓練知識補充內容
這可能造成錯誤資訊

解決方向

  • 明確要求只能使用 Context
  • 當資料不足時,要求模型說明
  • 設計回答驗證機制
  • 對關鍵資訊進行程式邏輯檢查
  • 進行多組測試,而不是只測試一個問題

問題二:搜尋結果與問題不相關

如果 Chunking 或 Embedding 效果不佳,Vector Search 可能找不到正確資料

這時候 LLM 即使能力很強,也沒有足夠的參考資料回答問題

解決方向

  • 調整 Chunk Size
  • 調整 Chunk Overlap
  • 選擇適合的 Embedding Model
  • 調整 Top-K
  • 增加文件 Metadata
  • 評估搜尋結果的相關性

RAG 的品質不只取決於 LLM,也取決於前面的 Retrieval

Context 太長

如果搜尋結果包含太多文字,可能造成:

  • Prompt Token 數增加
  • API 成本提高
  • 回答速度變慢
  • 模型注意力分散
  • 不相關內容干擾回答

解決方向

  • 控制 Top-K 數量
  • 避免重複文件
  • 調整 Chunk Size
  • 只保留相關內容
  • 後續加入 Reranking 機制

重複建立 Vector Database

如果每次執行程式都新增相同資料,可能產生重複內容

解決方向

為每個 Chunk 建立固定 ID

不過在正式系統中,仍應該根據文件版本、內容雜湊值或唯一識別碼設計更完整的資料管理方式

今天學到什麼

今天完成了第一個基本的 RAG Pipeline,將前幾天的功能串接起來

重要學習內容包括:

  1. Vector Search 負責尋找相關資料
  2. Context 負責提供 LLM 參考內容
  3. Prompt 負責規範模型的回答方式
  4. LLM 負責理解 Context 並產生回答
  5. Retrieval 品質會直接影響最終答案
  6. Prompt 不能完全保證模型不會產生錯誤資訊
  7. RAG 系統需要持續測試與評估

今天最大的收穫是:

RAG 不只是把文件丟給 LLM,而是透過搜尋、資料組合與提示設計,建立一個有依據的回答流程


今天的系統完成到哪裡

我們已經從單純的 LLM API 呼叫,進一步建立了具備外部知識檢索能力的 AI 助理

但目前仍然存在一些限制:

  • 尚未提供引用來源
  • 尚未建立完整的錯誤處理
  • 尚未評估 Retrieval Precision
  • 尚未處理多輪對話
  • 尚未加入對話記憶
  • 尚未建立 API 服務

明天要做什麼

今天的 RAG Assistant 已經可以根據知識庫回答問題

下一步,我們要進一步思考:

如果使用者想知道答案來自哪一份文件,系統能不能提供引用來源

因此,Day 7 將會探討:

  • RAG 回答引用來源
  • Metadata 的實際用途
  • 如何顯示文件名稱與 Chunk 資訊
  • 如何降低無法追溯回答來源的問題
  • 建立具備基本引用功能的 RAG Assistant

從「能夠回答問題」,進一步走向「能夠說明答案依據」


上一篇
[Day 5] 建立第一個 Vector Database:讓 AI 助理搜尋自己的知識庫
系列文
AI 不只會回答:30 天打造一套真正能上線的智慧助理6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言